JavaScript 学习笔记之继承


说继承之前,我们回顾一下上一篇博文所提到的构造函数、原型、实例对象的关系

1
2
3
4
5
6
7
8
var CreateFun = function (name) {
this.name = name;
};//构造函数 CreateFun
CreateFun.prototype.sayName = function() {
alert(this.name);
};//原型对象 CreateFun.prototype
var eyesim = new CreateFun('Eyesim');//实例对象 eyesim
CreateFun.prototype.constructor === CreateFun;//true

每个构造函数都有一个原型对象,原型对象都默认包含这一个 constructor 的指针指向这个构造函数,而每个实例都包含一个指向原型对象的内部指针,这个时候,我们让原型对象等于另一类的实例,那么此时的原型对象就包含着一个指向另一个原型的指针,相应地,另一个原型也包含这一个指向另一个构造函数的指针,再假如这个另一个的原型又是另一个类型的实例,那么上述关系层层递进,就构成了实例与原型的链条,也就是原型链。

一、原型链

下面我们可以借助代码来描述一下,原型链的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function SuperType() {
this.property = true;
}//定义一个构造函数 SuperType()

SuperType.prototype.getSuperValue = function() {
return this.property;
};//给 SuperType 的原型对象添加一个 getSuperType 的方法

function SubType() {
this.subproperty = false;
}//定义另一个构造函数 SubType()

SubType.prototype = new SuperType();//SubType 的原型对象为 SuperType 构造函数的实例对象 ,也就是 SubType 继承了 SuperType

SubType.prototype.getSubValue = function() {
return this.subproperty;
};//给这个 SubType 的原型对象添加 getSubValue 方法

var instance = new SubType(); //instance 为 SubType 的实例对象

alert(instance.getSuperValue());//true 按道理,instance 继承了 SubType 的原型对象的方法,也沿着原型链可以访问到 SubType 的原型对象的原型对象的方法,所以应返回 true

我们尝试用图来描述以上代码 instance 与 SubType 构造函数、subType 的原型对象、SuperType构造函数、SuperType 的原型对象间的关系:
SuperType实例原型关系图

我们放大来看来,原型链就如下图所示:
原型链

当属性或方法不存在本实例身上时,搜索就会一层一层地往上找,直到找到原型链末端为止,当然,链的末端就是 Object 构造函数所指向的那个 prototype 原型对象。

另外,对于原型链,我们要注意以下几点:
1.所有的引用类型都默认继承了 Object,所有函数的默认原型都是 Object 的实例
所以默认原型都会包含一个内部指针指向 Object.prototype 这也就是为什么所有自定义的类型都会继承 toString()、valueOf() 等默认方法的原因。
2.确定原型和实例的关系:instanceof 操作符与 isPrototypeOf()方法
于是,对于上面的例子,就有

1
2
3
4
5
6
alert(instance instanceof Object);//true
alert(instance instanceof SuperType);//true
alert(instance instanceof SubType);//true
alert(Object.prototype.isPrototypeOf(instance));//true
alert(Object.prototype.isPrototypeOf(SuperType));//true
alert(Object.prototype.isPrototypeOf(SubType));//true

3.谨慎地定义方法

给原型添加方法的代码一定要放在替换原型的语句之后,为什么?我们看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
SubType.prototype = new SuperType();//替换原型
SubType.prototype.getSubValue = function() {
return this.subproperty;
};//添加新方法
SubType.prototype.getSuperValue = function() {
return false;
}//重写了超类型中的方法
var instance = new SubType();

alert(instance.getSuperValue());//false

再对比后替换的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}

SubType.prototype.getSubValue = function() {
return this.subproperty;
};
SubType.prototype.getSuperValue = function() {
return false;
}
SubType.prototype = new SuperType();//原型替换在添加方法后头
var instance = new SubType();

alert(instance.getSuperValue());//true

在先替换的例子里面, SubType 的原型在替换原型后才添加方法,这里涉及到一个先后顺序问题,首先我们看,一开始的时候,我们给 SuperType 的原型对象添加了方法 getSuperValue ,然后我们将 SubType 的默认的原型对象给替换掉,换成 SuperType 的实例对象,这个时候,SubType 也就继承了 SuperType 的原型对象的 getSuperType 的方法,然而我们又希望我们从 SubType 访问到的 getSuperType 的方法与原先 SuperType 不一样,即在 SubType 上重写 getSuperType ,于是我们对 geSuperType 的方法进行了重写,并且成功地如我们所愿地显示为 false ,实际上,我们在开发者平台上面打印出 instance 的时候,可以看到它的结构上,原来的 SuperType.prototype 的 getSuperValue 的方法还在,只不过,在原型链上,根据原型链搜索的特性,是从下往顶部走,所以我们给 SubType.prototype 添加的 getSuperValue 的方法先被找到,于是就显示 SubType.prototype 的 getSuperValue 的结果。
而在后替换的例子里面,无论前面 SubType.prototype 定义或添加的是什么, SubType.prototype 都是完整地被 SuperType 的实例替换掉,所以在原型链上就找不到 SubType.prototype 所添加的 getSuperValue 的方法,然后搜索往上继续寻找,直到找到 SuperType.prototype 中的 getSuperType 方法,并将其结果显示出来。
所以,不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
还有一点,上一章提到的定义原型对象时尽量不要用字面量的写法,因为会重写原型对象。
4.原型链的问题
尽管原型链很强大,但是它也存在一些问题,还记得上一章我们提到的包含引用类型值的原型问题吗,由于实例会对其共享一个地址,所以任意实例对其的改变会影响到其他实例的结果。
另外还有一个问题是,在创建子类型的实例的时候,不能向超类型的构造函数传递参数,因为不能通过传参在父类中定义属性,在父类中定义不符合面向对象编程的规则,属性应该由实例来定义,如果在父类定义,就会强制给所有实例继承这个属性。

二、借用构造函数

ECMAScript 将原型链作为实现继承的主要方法,但在实践中原型链存在着包含引用类型值的问题,所以一般我们很少会在实践中单独使用原型链,类似于上一章我们解决在创建对象时原型对象中包含引用类型值带来的问题,为了解决引用类型值问题,开发人员开始使用一种叫做借用构造函数的方法,即在子类型构造函数的内部调用父类构造函数。

1
2
3
4
5
6
7
8
9
10
11
function SuperType() {
this.colors = ["red","blue","green"];
}
function SubType() {
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);//"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors);//"red,blue,green"

借用构造函数中,子类通过调用父类构造函数而完成了继承,这种方法可以为每一个子类创建单独的属性,但对于需要共享的属性,也被单独创建了,但这个做法就是每次实例化都重新创建和定义所有的属性,这个失去了代码的可复用性,而且父类的原型对象的所定义的内容并不会被继承到子类去,只有父类构造函数中所定义的东西才会被继承。

1
2
3
4
5
6
7
8
9
10
function SuperType() {
this.colors = ["red","blue","green"];
}
SuperType.prototype.name = "eyesim";
function SubType() {
SuperType.call(this);
}
var instance1 = new SubType();
alert(instance1.colors);//"red,blue,green"
alert(instance1.name);//undefined

所以一般这种继承方式也是不被推荐的。

三、组合继承

组合继承指的是将原型链与借用构造函数的技术组合在一起,利用原型链实现原型属性和方法的继承,通过构造函数来实现对实例属性的继承。下面我们来看一下具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
alert(this.name);
}//父类的原型对象
function SubType(name,age) {
SuperType.call(this,name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
}// 子类的原型对象

//实例1
var instance1 = new SubType("Eyesim",22);
instance1.colors.push("blcak");
alert(instance1.colors);//"red,blue,green,black"
instance1.sayName();//Eyesim
instance1.sayAge();//22

//实例2
var instance2 = new SubType("Gordon",23);
alert(instance2.colors);//"red,blue,green"
instance2.sayName();//Gordon
instance2.sayAge();//23

我们来分析一下上面的代码,我们定义了一个父类构造函数 SuperType() ,然后给这个 SuperType 的原型对象添加了一个 SayName 的方法,然后我们用借用构造函数的方法让子类 SybType 继承了 SuperType ,之后再利用原型链让 SubType 的原型对象等于 SuperType 的实例对象,但是注意一点,实际上 SubType 的原型对象的 constuctor 指针应该指向 SubType 构造函数才对,但在该语句 SubType.prototype = new SuperType() 的操作后,SubType 的原型对象的 constuctor 指针指向了 SuperType ,所以下面我们再用了语句 SubType.prototype.constructor = SubType 修改回来,之后我们又给 SubType 的原型对象添加了一个方法 SayName 来测试继承后的结果,接下来我们继续看一下下面的实例 instance1 与 instance2 ,instance1 在对 colors 属性进行了操作添加了一个 black ,但是访问 instance1 与 instance2 的 colors 属性时,两个结果是不一样的,可见这种组合方式修补了原型链继承方法中遇到引用类型值时任意一实例对象该类型的属性进行修改会影响其他实例的缺陷,我们继续看一下,SubType 的原型对象以原型链的方式继承了 SuperType 后,我们可以在实例中访问到了 SuperType 中的 SayName 的方法,而且也能访问到 SubType 原型对象的 SayAge 方法,这一点修补了借用构造函数的继承模式访问子类的实例直接访问不了父类的原型对象中的属性与方法的缺陷。因此,组合继承成为了 JavaScript 中最常用的继承模式。而且 instanceof 与 isPrototypeOf() 也能够识别基于组合继承创建的对象。

四、原型式继承

这里说的原型式继承与上面提到的原型链继承是两种不一样的模式,原型式继承相当于利于一个现有的对象进行浅拷贝,下面我们来看看实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var person = {
name:"eyesim",
friends:["Gordon","Ken","Hedy"]
};
var anotherPerson = object(person);
anotherPerson.name = "Javis";
anotherPerson.friends.push("Jacy");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Ivy";
yetAnotherPerson.friends.push("Gilbert");
alert(person.friends);//Gordon,Ken,Hedy,Jacy,Gilbert

原型式继承要求你必须有一个对象可以作为另一个对象的基础,将这个对象传给 object() 函数,然后该函数就会返回一个新的对象,这个对象将以 person 为原型,但要注意的是,原型式继承方法与原型链在处理引用值上是一样的,都是共有一套数据。另外,ECMAScript5通过新增了 Object.create() 的方法规范了原型式继承,如果当传人的参数为一个的时候,他的作用与上面的 object() 方法行为是一样的,那么传入的参数为两个的时候是怎样的呢?

1
2
3
4
5
6
7
8
9
10
var person = {
name:"eyesim",
friends:["Gordon","Ken","Hedy"]
};
var anothetPerson = Object.create(person,{
name:{
value: "EyesiM"
}
});
alert(anothetPerson.name);//Eyesim

实际上,第二个参数是给实例自己添加属性,根据原型链,先找自己的属性再往原型上找,所以便“覆盖”了原型的 name 属性。

五、寄生式继承

寄生式继承模式的思路与我们在说创建对象的时候的寄生构造函数和工厂模式很像,就是创建一个仅用于封装继承过程的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function object (o) {
function F () {}
F.prototype = o;
return new F ();
}
function createObject (origin) {
var clone = object(origin);
clone.sayHi = function () {
alert('hi');
};
return clone;
}
var person = {
name:"eyesim",
friends:["Gordon","Ken","Hedy"]
};
var anotherPerson = createObject(person);
anotherPerson.sayHi();

在这个例子中,object 函数不是必需的,任何能够返回新对象的函数都能取代它,同样注意:使用寄生式继承来为对象添加函数,会由于不能做到函数的复用而降低效率。

六、寄生组合式继承

前面有说组合模式是最常用的继承模式,但它也有不足,组合模式最大的问题就是,无论在任何情况下都会两次调用父类构造函数,一次是在创建子类原型的时候,另外一次就是在子类型构造函数内部,这使得我们在调用子类型构造函数的时候重写这些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
alert(this.name);
}//父类的原型对象
function SubType(name,age) {
SuperType.call(this,name);//第一次调用父类构造函数 SuperType()
this.age = age;
}
SubType.prototype = new SuperType();//第二次调用父类构造函数 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
alert(this.age);
}// 子类的原型对象

上面的例子中,子类函数内部与原型创建过程中都各自调用了一次父类构造函数,这会使得子类的实例和原型将会拥有两套完全相同的来自父类型的属性,这样会造成不必要的性能以及内存浪费。而寄生组合式继承就是为了处理这个问题而出现的。寄生组合模式的基本模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function inheritPrototype(subType,superType){
var prototype = Object(superType.prototype);//取出一份父类型原型的副本
prototype.constructor = subType;//让实例跟原型重新联系起来
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
alert(this.name);
}
function SubType(name,age) {
SuperType.call(this,name);
this.age = age;
}

inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function() {
alert(this.age);
}// 子类的原型对象

这个 inheritPrototype() 方法取代了原型链继承,实际上是取一份父类原型的对象的副本,而没用到真正的父类原型对象,这种方法避免了两次调用父类构造函数,消除了组合模式的缺点,也保持了了原型链不变,还能正常使用 instanceof 和 isPrototypeOf() ,因此寄生组合式继承被认为是引用类型继承最理想的范式。